<template>
  <div
    class="vue-pull-to-wrapper"
    :style="{
      height: wrapperHeight,
      transform: `translate3d(0, ${diff}px, 0)`
    }"
  >
    <div
      v-if="topLoadMethod"
      :style="{
        height: `${topBlockHeight}px`,
        marginTop: `${-topBlockHeight}px`
      }"
      class="action-block"
    >
      <slot
        name="top-block"
        :state="state"
        :state-text="topText"
        :trigger-distance="_topConfig.triggerDistance"
        :diff="diff"
      >
        <p class="default-text">{{ topText }}</p>
      </slot>
    </div>
    <div class="scroll-container">
      <slot></slot>
    </div>
    <div
      v-if="bottomLoadMethod"
      :style="{
        height: `${bottomBlockHeight}px`,
        marginBottom: `${-bottomBlockHeight}px`
      }"
      class="action-block"
    >
      <slot
        name="bottom-block"
        :state="state"
        :state-text="bottomText"
        :trigger-distance="_bottomConfig.triggerDistance"
        :diff="diff"
      >
        <p class="default-text">{{ bottomText }}</p>
      </slot>
    </div>
  </div>
</template>

<script>
/* eslint-disable vue/require-default-prop */

const TOP_DEFAULT_CONFIG = {
  pullText: '下拉刷新',
  triggerText: '释放更新',
  loadingText: '加载中...',
  doneText: '加载完成',
  failText: '加载失败',
  loadedStayTime: 400,
  stayDistance: 50,
  triggerDistance: 70
}

const BOTTOM_DEFAULT_CONFIG = {
  pullText: '上拉加载',
  triggerText: '释放更新',
  loadingText: '加载中...',
  doneText: '加载完成',
  failText: '加载失败',
  loadedStayTime: 400,
  stayDistance: 50,
  triggerDistance: 70
}
export default {
  props: {
    distanceIndex: {
      // 滑动的阀值（值越大滑动的速度越慢）
      type: Number,
      default: 2
    },
    topBlockHeight: {
      // 顶部在滚动容器外的块级元素区域高度
      type: Number,
      default: 50
    },
    bottomBlockHeight: {
      // 底部在滚动容器外的块级元素区域高度
      type: Number,
      default: 50
    },
    wrapperHeight: {
      // 滚动容器的高度
      type: String,
      default: '100%'
    },
    topLoadMethod: {
      // 顶部下拉时执行的方法
      type: Function
    },
    bottomLoadMethod: {
      // 底部上拉时执行的方法
      type: Function
    },
    isThrottleTopPull: {
      // 是否截流top-pull事件的触发以保证性能，如果需要实时触发设为false
      type: Boolean,
      default: true
    },
    isThrottleBottomPull: {
      // 是否截流bottom-pull事件的触发以保证性能，如果需要实时触发设为false
      type: Boolean,
      default: true
    },
    isThrottleScroll: {
      // 是否截流scroll事件的触发以保证性能，如果需要实时触发设为false
      type: Boolean,
      default: true
    },
    isTopBounce: {
      // 是否启用下拉回弹效果
      type: Boolean,
      default: true
    },
    isBottomBounce: {
      // 是否启用上拉回弹效果
      type: Boolean,
      default: true
    },
    topConfig: {
      // 滚动容器顶部信息的一些配置
      type: Object,
      default: () => {
        return {}
      }
    },
    bottomConfig: {
      // 滚动容器底部信息的一些配置
      type: Object,
      default: () => {
        return {}
      }
    },
    noMore: {
      // 下拉加载没更多数据
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      scrollEl: null, // 挂载节点
      startScrollTop: 0, // 到顶部的距离
      startY: 0, // touchstart Y的位置
      currentY: 0, // touchmove Y的位置
      distance: 0, // 滑动的距离
      direction: 0, // 方向 distance大于0为down、小于0为up
      diff: 0, // 滑动差值(触发加载时才有值)
      beforeDiff: 0,
      topText: '', // 顶部加载文字
      bottomText: '', // 底部加载文字
      state: '', // pull、down、loading 状态
      bottomReached: false, // 到达底部标识
      throttleEmitTopPull: null, // 下拉节流
      throttleEmitBottomPull: null, // 上拉节流
      throttleEmitScroll: null, // 滚动节流
      throttleOnInfiniteScroll: null, // 滚动底部加载更多节流
      pageY: 0 // 用于记录当前页面滚动位置 keep-alive
    }
  },
  computed: {
    _topConfig: function() {
      return Object.assign({}, TOP_DEFAULT_CONFIG, this.topConfig)
    },
    _bottomConfig: function() {
      return Object.assign({}, BOTTOM_DEFAULT_CONFIG, this.bottomConfig)
    }
  },
  watch: {
    state(val) {
      if (this.direction === 'down') {
        this.$emit('top-state-change', val)
      } else {
        this.$emit('bottom-state-change', val)
      }
    }
  },
  mounted() {
    this.init()
  },
  activated() {
    this.scrollEl.scrollTop = this.pageY
  },
  methods: {
    init() {
      this.createThrottleMethods()
      this.scrollEl = this.$el.querySelector('.scroll-container')
      this.bindEvents()
    },
    createThrottleMethods() {
      this.throttleEmitTopPull = this.throttleEmit(200, 300, 'top-pull')
      this.throttleEmitBottomPull = this.throttleEmit(200, 300, 'bottom-pull')
      this.throttleEmitScroll = this.throttleEmit(100, 150, 'scroll')
      this.throttleOnInfiniteScroll = this.throttle(this.onInfiniteScroll, 400)
    },
    throttleEmit(delay, mustRunDelay = 0, eventName) {
      const throttleMethod = function() {
        const args = [...arguments]
        args.unshift(eventName)
        this.$emit.apply(this, args)
      }
      return this.throttle(throttleMethod, delay, mustRunDelay)
    },
    bindEvents() {
      this.scrollEl.addEventListener('touchstart', this.handleTouchStart)
      this.scrollEl.addEventListener('touchmove', this.handleTouchMove)
      this.scrollEl.addEventListener('touchend', this.handleTouchEnd)
      this.scrollEl.addEventListener('scroll', this.handleScroll)
    },
    // 节流
    throttle(fn, delay, mustRunDelay = 0) {
      let timer = null
      let tStart
      return function() {
        const context = this
        const args = arguments
        const tCurr = +new Date()
        clearTimeout(timer)
        if (!tStart) {
          tStart = tCurr
        }
        if (mustRunDelay !== 0 && tCurr - tStart >= mustRunDelay) {
          fn.apply(context, args)
          tStart = tCurr
        } else {
          timer = setTimeout(function() {
            fn.apply(context, args)
          }, delay)
        }
      }
    },
    onInfiniteScroll() {
      if (this.checkBottomReached()) {
        if (!this.noMore) {
          this.$emit('infinite-scroll')
        }
      }
    },
    checkBottomReached() {
      this.pageY = this.scrollEl.scrollTop
      return (
        this.scrollEl.scrollTop + this.scrollEl.offsetHeight + 1 >=
        this.scrollEl.scrollHeight
      )
    },
    handleTouchStart(event) {
      this.startY = event.touches[0].clientY
      this.beforeDiff = this.diff
      this.startScrollTop = this.scrollEl.scrollTop
      this.bottomReached = this.checkBottomReached()
    },
    handleTouchMove(event) {
      this.currentY = event.touches[0].clientY
      this.distance =
        (this.currentY - this.startY) / this.distanceIndex + this.beforeDiff
      this.direction = this.distance > 0 ? 'down' : 'up'
      if (
        this.startScrollTop === 0 &&
        this.direction === 'down' &&
        this.isTopBounce
      ) {
        event.preventDefault()
        event.stopPropagation()
        this.diff = this.distance
        this.isThrottleTopPull
          ? this.throttleEmitTopPull(this.diff)
          : this.$emit('top-pull', this.diff)
        if (typeof this.topLoadMethod !== 'function') return
        if (
          this.distance < this._topConfig.triggerDistance &&
          this.state !== 'pull' &&
          this.state !== 'loading'
        ) {
          this.actionPull()
        } else if (
          this.distance >= this._topConfig.triggerDistance &&
          this.state !== 'trigger' &&
          this.state !== 'loading'
        ) {
          this.actionTrigger()
        }
      } else if (
        this.bottomReached &&
        this.direction === 'up' &&
        this.isBottomBounce
      ) {
        event.preventDefault()
        event.stopPropagation()
        this.diff = this.distance
        this.isThrottleBottomPull
          ? this.throttleEmitBottomPull(this.diff)
          : this.$emit('bottom-pull', this.diff)
        if (typeof this.bottomLoadMethod !== 'function') return
        if (
          Math.abs(this.distance) < this._bottomConfig.triggerDistance &&
          this.state !== 'pull' &&
          this.state !== 'loading'
        ) {
          this.actionPull()
        } else if (
          Math.abs(this.distance) >= this._bottomConfig.triggerDistance &&
          this.state !== 'trigger' &&
          this.state !== 'loading'
        ) {
          this.actionTrigger()
        }
      }
    },
    handleTouchEnd() {
      if (this.diff !== 0) {
        if (this.state === 'trigger') {
          this.actionLoading()
          return
        }
        // pull cancel
        this.scrollTo(0)
      }
    },
    handleScroll(event) {
      this.isThrottleScroll
        ? this.throttleEmitScroll(event)
        : this.$emit('scroll', event)
      this.throttleOnInfiniteScroll()
    },
    scrollTo(y, duration = 200) {
      this.$el.style.transition = `${duration}ms`
      this.diff = y
      setTimeout(() => {
        this.$el.style.transition = ''
      }, duration)
    },
    actionPull() {
      this.state = 'pull'
      this.direction === 'down'
        ? (this.topText = this._topConfig.pullText)
        : (this.bottomText = this._bottomConfig.pullText)
    },
    actionTrigger() {
      this.state = 'trigger'
      this.direction === 'down'
        ? (this.topText = this._topConfig.triggerText)
        : (this.bottomText = this._bottomConfig.triggerText)
    },
    actionLoading() {
      this.state = 'loading'
      if (this.direction === 'down') {
        this.topText = this._topConfig.loadingText
        /* eslint-disable no-useless-call */
        this.topLoadMethod.call(this, this.actionLoaded)
        this.scrollTo(this._topConfig.stayDistance)
      } else {
        this.bottomText = this._bottomConfig.loadingText
        this.bottomLoadMethod.call(this, this.actionLoaded)
        this.scrollTo(-this._bottomConfig.stayDistance)
      }
    },
    actionLoaded(loadState = 'done') {
      this.state = `loaded-${loadState}`
      let loadedStayTime
      if (this.direction === 'down') {
        this.topText =
          loadState === 'done'
            ? this._topConfig.doneText
            : this._topConfig.failText
        loadedStayTime = this._topConfig.loadedStayTime
      } else {
        this.bottomText =
          loadState === 'done'
            ? this._bottomConfig.doneText
            : this._bottomConfig.failText
        loadedStayTime = this._bottomConfig.loadedStayTime
      }
      setTimeout(() => {
        this.scrollTo(0)
        // reset state
        setTimeout(() => {
          this.state = ''
        }, 200)
      }, loadedStayTime)
    }
  }
}
</script>

<style scoped>
.vue-pull-to-wrapper {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.scroll-container {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.vue-pull-to-wrapper .action-block {
  position: relative;
  width: 100%;
}

.default-text {
  height: 100%;
  line-height: 50px;
  text-align: center;
}
</style>
